/**
 * @license
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import '../../elements/shared/gr-tooltip/gr-tooltip';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
import {getRootElement} from '../../scripts/rootElement';
import {property, observe} from '@polymer/decorators';
import {GrTooltip} from '../../elements/shared/gr-tooltip/gr-tooltip';
import {PolymerElement} from '@polymer/polymer';
import {Constructor} from '../../utils/common-util';

const BOTTOM_OFFSET = 7.2; // Height of the arrow in tooltip.

/** The interface corresponding to TooltipMixin */
export interface TooltipMixinInterface {
  hasTooltip: boolean;
  positionBelow: boolean;
  _isTouchDevice: boolean;
  _tooltip: GrTooltip | null;
  _titleText: string;
  _hasSetupTooltipListeners: boolean;
}

/**
 * @polymer
 * @mixinFunction
 */
export const TooltipMixin = <T extends Constructor<PolymerElement>>(
  superClass: T
) => {
  /**
   * @polymer
   * @mixinClass
   */
  class Mixin extends superClass {
    @property({type: Boolean})
    hasTooltip = false;

    @property({type: Boolean, reflectToAttribute: true})
    positionBelow = false;

    @property({type: Boolean})
    _isTouchDevice = 'ontouchstart' in document.documentElement;

    @property({type: Object})
    _tooltip: GrTooltip | null = null;

    @property({type: String})
    _titleText = '';

    @property({type: Boolean})
    _hasSetupTooltipListeners = false;

    // Handler for mouseenter event
    private mouseenterHandler?: (e: MouseEvent) => void;

    // Handler for scrolling on window
    private readonly windowScrollHandler: () => void;

    // Handler for showing the tooltip, will be attached to certain events
    private readonly showHandler: () => void;

    // Handler for hiding the tooltip, will be attached to certain events
    private readonly hideHandler: (e: Event) => void;

    // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
    constructor(..._: any[]) {
      super();
      this.windowScrollHandler = () => this._handleWindowScroll();
      this.showHandler = () => this._handleShowTooltip();
      this.hideHandler = (e: Event | undefined) => this._handleHideTooltip(e);
    }

    override disconnectedCallback() {
      // NOTE: if you define your own `detached` in your component
      // then this won't take affect (as its not a class yet)
      this._handleHideTooltip(undefined);
      if (this.mouseenterHandler) {
        this.removeEventListener('mouseenter', this.mouseenterHandler);
      }
      window.removeEventListener('scroll', this.windowScrollHandler);
      super.disconnectedCallback();
    }

    @observe('hasTooltip')
    _setupTooltipListeners() {
      if (!this.mouseenterHandler) {
        this.mouseenterHandler = this.showHandler;
      }

      if (!this.hasTooltip) {
        // if attribute set to false, remove the listener
        this.removeEventListener('mouseenter', this.mouseenterHandler);
        this._hasSetupTooltipListeners = false;
        return;
      }

      if (this._hasSetupTooltipListeners) {
        return;
      }
      this._hasSetupTooltipListeners = true;

      this.addEventListener('mouseenter', this.mouseenterHandler);
    }

    _handleShowTooltip() {
      if (this._isTouchDevice) {
        return;
      }

      if (
        !this.hasAttribute('title') ||
        this.getAttribute('title') === '' ||
        this._tooltip
      ) {
        return;
      }

      // Store the title attribute text then set it to an empty string to
      // prevent it from showing natively.
      this._titleText = this.getAttribute('title') || '';
      this.setAttribute('title', '');

      const tooltip = document.createElement('gr-tooltip');
      tooltip.text = this._titleText;
      tooltip.maxWidth = this.getAttribute('max-width') || '';
      tooltip.positionBelow = this.hasAttribute('position-below');

      // Set visibility to hidden before appending to the DOM so that
      // calculations can be made based on the element’s size.
      tooltip.style.visibility = 'hidden';
      getRootElement().appendChild(tooltip);
      this._positionTooltip(tooltip);
      tooltip.style.visibility = 'initial';

      this._tooltip = tooltip;
      window.addEventListener('scroll', this.windowScrollHandler);
      this.addEventListener('mouseleave', this.hideHandler);
      this.addEventListener('click', this.hideHandler);
      tooltip.addEventListener('mouseleave', this.hideHandler);
    }

    _handleHideTooltip(e: Event | undefined) {
      if (this._isTouchDevice) {
        return;
      }
      if (!this.hasAttribute('title') || !this._titleText) {
        return;
      }
      // Do not hide if mouse left this or this._tooltip and came to this or
      // this._tooltip
      if (
        (e as MouseEvent)?.relatedTarget === this._tooltip ||
        (e as MouseEvent)?.relatedTarget === this
      ) {
        return;
      }

      window.removeEventListener('scroll', this.windowScrollHandler);
      this.removeEventListener('mouseleave', this.hideHandler);
      this.removeEventListener('click', this.hideHandler);
      this.setAttribute('title', this._titleText);
      this._tooltip?.removeEventListener('mouseleave', this.hideHandler);

      if (this._tooltip?.parentNode) {
        this._tooltip.parentNode.removeChild(this._tooltip);
      }
      this._tooltip = null;
    }

    _handleWindowScroll() {
      if (!this._tooltip) {
        return;
      }

      this._positionTooltip(this._tooltip);
    }

    _positionTooltip(tooltip: GrTooltip) {
      // This flush is needed for tooltips to be positioned correctly in Firefox
      // and Safari.
      flush();
      const rect = this.getBoundingClientRect();
      const boxRect = tooltip.getBoundingClientRect();
      if (!tooltip.parentElement) {
        return;
      }
      const parentRect = tooltip.parentElement.getBoundingClientRect();
      const top = rect.top - parentRect.top;
      const left =
        rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
      const right = parentRect.width - left - boxRect.width;
      if (left < 0) {
        tooltip.updateStyles({
          '--gr-tooltip-arrow-center-offset': `${left}px`,
        });
      } else if (right < 0) {
        tooltip.updateStyles({
          '--gr-tooltip-arrow-center-offset': `${-0.5 * right}px`,
        });
      }
      tooltip.style.left = `${Math.max(0, left)}px`;

      if (!this.positionBelow) {
        tooltip.style.top = `${Math.max(0, top)}px`;
        tooltip.style.transform = `translateY(calc(-100% - ${BOTTOM_OFFSET}px))`;
      } else {
        tooltip.style.top = `${top + rect.height + BOTTOM_OFFSET}px`;
      }
    }
  }

  return Mixin as T & Constructor<TooltipMixinInterface>;
};
